"""
Map editor

This editor edits extremely basic tile maps.

[ Version .1 ]
For this version, you can simply edit the value of the map at a given location.
A grid would probably work for this.

[ Version .2 ]
Each tile will contain a map item with a set of properties.
 - You can choose objects from your object library to add to the map.
 - A property view allows you to edit objects that you select.
 - You can select objects in a rectangle with [SHIFT]+drag,
   individually with a click, or add an object to the selection with [CTRL]+click.
   The property view will allow you to edit objects with similar properties.
 - You can supply a view to use for objects from your object library.
   The view must act like a StampView, accepting the object and showing a
   representation of it.  Optionally, you can use a StampView itself
   and pass in views as you want.

[ Version .3]
 - Allow editing of view properties from the map editor (in-context editing???)

"""

import wx, os
from editor import Editor
from editors import register_opener, register_factory, EditorFactoryInfo
import wx.lib.sized_controls as sc
import wx.lib.filebrowsebutton as filebrowse

class TileMapFactory(sc.SizedDialog):
	def __init__(self, parent):
		# Initialize sized dialog
		sc.SizedDialog.__init__(self, parent, title = "New Tile Map",
		                        style = wx.DEFAULT_DIALOG_STYLE |
		                        wx.RESIZE_BORDER)
		pane = self.GetContentsPane()
		pane.SetSizerType("form")
		
		# File name
		# TODO: consider adding
		#wx.StaticText(pane, -1, "File name:")

		#filePane = sc.SizedPanel(pane, -1)
		#filePane.SetSizerType("horizontal")
		#filePane.SetSizerProps(expand=True)
		#self.file_name_picker = filebrowse.FileBrowseButton(filePane,
		#	-1, size=(300, -1), labelText = "")

		# Tiles across
		wx.StaticText(pane, -1, "Tiles across:")
		self.width_text = wx.TextCtrl(pane, -1, "20")

		# Tiles down
		wx.StaticText(pane, -1, "Tiles down:")
		self.height_text = wx.TextCtrl(pane, -1, "20")
		
		# Initial cell value
		wx.StaticText(pane, -1, "Initial cell value:")
		self.initial_value_text = wx.TextCtrl(pane, -1, "0")
		
		# Add dialog buttons
		self.SetButtonSizer(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL))

		self.Fit()
		self.SetMinSize(self.GetSize())

	def make_tile_map(self):
		# TODO: validation
		return TileMap(width         = int(self.width_text.GetValue()),
		               height        = int(self.height_text.GetValue()),
		               initial_value = int(self.initial_value_text.GetValue()))

class TileSetFactory(sc.SizedDialog):
	"""
	Loads a TileSet from an existing image and the given tile dimensions.
	"""
	def __init__(self, parent):
		# Initialize sized dialog
		sc.SizedDialog.__init__(self, parent, title = "Choose Tile Set",
		                        style = wx.DEFAULT_DIALOG_STYLE |
		                        wx.RESIZE_BORDER)
		pane = self.GetContentsPane()
		pane.SetSizerType("form")
		
		# File name
		wx.StaticText(pane, -1, "Image with tile pictures:")

		filePane = sc.SizedPanel(pane, -1)
		filePane.SetSizerType("horizontal")
		filePane.SetSizerProps(expand=True)
		self.file_name_picker = filebrowse.FileBrowseButton(filePane,
		    -1, size=(300, -1), labelText = "")

		# Tile width
		wx.StaticText(pane, -1, "Tile width:")
		self.width_text = wx.TextCtrl(pane, -1, "32")

		# Tile height
		wx.StaticText(pane, -1, "Tile height:")
		self.height_text = wx.TextCtrl(pane, -1, "32")
		
		# Add dialog buttons
		self.SetButtonSizer(self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL))

		self.Fit()
		self.SetMinSize(self.GetSize())

	def make_tile_set(self):
		# TODO: validation
		return TileSet(image = wx.Image(self.file_name_picker.GetValue()),
		               tile_size = (int(self.width_text.GetValue()),
		                            int(self.height_text.GetValue())))

class TileSet(object):
	"""
	Stores and returns tiles as wx.Bitmap objects, for display on the canvas.
	"""
	def __init__(self, image, tile_size = (32, 32)):
		"""
		Create a new TileSet from the given wx.Image containing tiles,
		and tile size.
		"""
		self.image = wx.BitmapFromImage(image)

		self.tile_size = tile_size
		self.set_size = (self.image.Width / tile_size[0],
		                 self.image.Height / tile_size[1])

		self.tile_cache = [[None] * self.set_size[0] for _ in
		                   xrange(self.set_size[1])]

	def get_tile(self, tile):
		"""
		Returns a wx.Bitmap for the tile with the given index.
		"""
		x, y = self.tile_to_position(tile)
		tile = self.tile_cache[y][x]
		if tile is None:
			tile = self.image.GetSubBitmap((x * self.tile_size[0],
			                                y * self.tile_size[1],
			                                self.tile_size[0],
			                                self.tile_size[1]))
			self.tile_cache[y][x] = tile
		return tile

	def tile_to_position(self, tile):
		return tile % self.set_size[0], tile / self.set_size[0]

	def position_to_tile(self, x, y):
		return y * self.set_size[0] + x

class TileMapCanvas(wx.ScrolledWindow):
	def __init__(self, parent):
		super(TileMapCanvas, self).__init__(parent, -1, (0, 0),
		                                    style = wx.SUNKEN_BORDER)
		self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
		self.parent = parent

		self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
		self.Bind(wx.EVT_MOTION   , self.on_mouse_motion)
		self.Bind(wx.EVT_PAINT    , self.on_paint)

		tile_set = self.parent.tile_set
		tile_width, tile_height = tile_set.tile_size
		tile_map = self.parent.tile_map
		self.SetVirtualSize((tile_map.width * tile_width,
		                     tile_map.height * tile_height))
		self.SetScrollRate(tile_width, tile_height)
		self.SetBackgroundColour("WHITE")

	def on_mouse_motion(self, event):
		if event.LeftIsDown():
			self.set_tile(event)

	def set_tile(self, event):
		tile_map = self.parent.tile_map
		tile_set = self.parent.tile_set
		tile_width, tile_height = tile_set.tile_size
		x, y = self.CalcUnscrolledPosition(event.GetX(), event.GetY())
		x /= tile_width
		y /= tile_height
		if x < 0 or x >= tile_map.width or y < 0 or y >= tile_map.height:
			return
		new_tile = self.parent.get_selected_tile()
		if tile_map.tiles[y][x] != new_tile:
			tile_map.tiles[y][x] = new_tile
			# TODO: use RefreshRect
			self.Refresh()

	def on_left_down(self, event):
		self.set_tile(event)

	def on_paint(self, event):
		dc = wx.PaintDC(self)
		self.PrepareDC(dc)
		dc.BeginDrawing()

		# Clear the background.
		dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
		dc.Clear()

		# Draw the cells.
		tile_set = self.parent.tile_set
		tile_width, tile_height = tile_set.tile_size
		for y, row in enumerate(self.parent.tile_map.tiles):
			for x, tile in enumerate(row):
				dc.DrawBitmap(tile_set.get_tile(tile),
				              x * tile_width,
				              y * tile_height,
				              True)

		dc.EndDrawing()
		dc.Destroy()

class TilePicker(wx.ScrolledWindow):
	def __init__(self, parent, editor):
		super(TilePicker, self).__init__(parent, -1, (0, 0),
		                                 style = wx.SUNKEN_BORDER)
		self.parent = parent
		self.editor = editor

		self.pane_info = (wx.aui.AuiPaneInfo()
		                  .Name("tile_picker").Caption("Tile Picker")
		                  .Left().MaximizeButton(True).CloseButton(False)
		                  .MinSize(wx.Size(200,200)).Layer(1))

		self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))

		self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
		self.Bind(wx.EVT_PAINT    , self.on_paint)

		tile_set = self.editor.tile_set
		tile_width, tile_height = tile_set.tile_size
		self.SetVirtualSize((tile_set.image.Width, tile_set.image.Height))
		self.SetScrollRate(tile_width, tile_height)

		self.SetBackgroundColour("WHITE")

		self.selected_tile = 0

	def on_left_down(self, event):
		tile_set = self.editor.tile_set
		tile_width, tile_height = tile_set.tile_size
		x, y = self.CalcUnscrolledPosition(event.GetX(), event.GetY())
		self.selected_tile = tile_set.position_to_tile((x / tile_width),
		                                               (y / tile_height))
		# TODO: use RefreshRect
		self.Refresh()

	def on_paint(self, event):
		dc = wx.PaintDC(self)
		self.PrepareDC(dc)
		dc.BeginDrawing()

		# Clear the background.
		dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
		dc.Clear()

		# Draw the cells.
		dc.DrawBitmap(self.editor.tile_set.image, 0, 0, True)

		# Draw the selected tile
		tile_set = self.editor.tile_set
		tile_width, tile_height = tile_set.tile_size
		x, y = tile_set.tile_to_position(self.selected_tile)
		dc.SetPen(wx.Pen('BLUE', 2))
		dc.SetBrush(wx.TRANSPARENT_BRUSH)
		dc.DrawRectangle(x * tile_width, y * tile_height, tile_width,
		                 tile_height)

		dc.EndDrawing()

	def close(self):
		# FIXME: don't "reach in" like this.
		self.parent._mgr.DetachPane(self)
		self.parent._mgr.Update()

	def open(self):
		# FIXME: don't "reach in" like this.
		self.parent._mgr.AddPane(self, self.pane_info)
		self.parent._mgr.Update()

class TileMapEditor(Editor):
	def __init__(self, parent, style = 0, tile_map = None, path = None,
	             tile_set = None):
		super(TileMapEditor, self).__init__(parent, style | wx.WANTS_CHARS,
		                                    bind_later = True)
		self.tile_map = tile_map
		self.tile_set = tile_set
		self.path = path

		if self.path is not None:
			self.title = os.path.basename(self.path)

		self.canvas = TileMapCanvas(self)

		self.tile_picker = TilePicker(self.parent, self)

		s = wx.BoxSizer(wx.HORIZONTAL)
		s.Add(self.canvas, 1, wx.ALL | wx.EXPAND)
		self.SetSizer(s)
		self.Layout()

		parent.bind_editor(self)

	def get_selected_tile(self):
		# TODO: use a property for this
		return self.tile_picker.selected_tile

	def focus(self):
		self.tile_picker.open()

	def blur(self):
		self.tile_picker.close()

	def save(self, save_as = False):
		# Get filename to save from the user.
		if self.path is None or save_as:
			# FIXME: this clears out the existing path on Save As if the
			# user cancels
			self.path = self.parent.show_file_dialog(wildcard =
				'Tile Map Files (*.map)|*.map')
			if self.path is None: return False
			self.title = os.path.basename(self.path)

		# Save the file.
		self.tile_map.save(self.path)

		return True

class TileMap(object):
	def __init__(self, path = None, width = 20, height = 20,
	             initial_value = 0):
		"""
		Creates a tile map from a file, or a new one with the given dimensions
		and initial cell value.
		"""
		if path:
			self.tiles = []
			with open(path) as f:
				for row in f:
					self.tiles.append(map(int, row.split()))
			# TODO: error checking
			self.width  = len(self.tiles[0])
			self.height = len(self.tiles)
		else:
			self.width  = width
			self.height = height
			self.tiles  = [[initial_value] * width for _ in xrange(height)]

	def save(self, path):
		with open(path, 'w') as f:
			for row in self.tiles:
				f.write(' '.join(map(str, row)) + '\n')

def pick_tile_set(app):
	# TODO: associate tile maps with tile sets
	# TODO: provide a way to create tile sets
	# TODO: validation
	dlg = TileSetFactory(app.main_win)
	try:
		if dlg.ShowModal() != wx.ID_OK: return None
		return dlg.make_tile_set()
	finally:
		dlg.Destroy()

def open_tile_map_file(app, path):
	"Opens a tile map file."
	file_name, file_ext = os.path.splitext(path)
	if file_ext != '.map': return False

	# TODO: check for errors
	TileMapEditor(app.main_win,
	              path = path,
	              tile_set = pick_tile_set(app),
	              tile_map = TileMap(path))

	return True

def create_tile_map_file(app):
	"Creates a new tile map file with user-specified dimensions."
	dlg = TileMapFactory(app.main_win)
	# TODO: check for errors.
	# TODO: make this a lot less tedious.
	try:
		if dlg.ShowModal() == wx.ID_OK:
			tile_set = pick_tile_set(app)
			if tile_set is not None:
				TileMapEditor(app.main_win,
				              tile_set = tile_set,
				              tile_map = dlg.make_tile_map())
	finally:
		dlg.Destroy()

register_opener(open_tile_map_file)
register_factory(EditorFactoryInfo('New Tile Map', 'tile map', create_tile_map_file))
